Skip to content

Commit bf8edd8

Browse files
authored
Merge branch 'main' into feature/pre-post-processing
2 parents dbe519d + 25c4792 commit bf8edd8

23 files changed

+400
-29
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ jobs:
1717
matrix:
1818
os: [ubuntu]
1919
ruby: ['3.2', '3.3', '3.4']
20+
gemfile: ['activesupport7', 'activesupport8']
2021
runs-on: ${{ matrix.os }}-latest
21-
continue-on-error: ${{ matrix.ruby == 'ruby-head' }}
22+
env:
23+
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
2224
steps:
2325
- uses: actions/checkout@v4
2426
- name: Install ripgrep

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ bin/ruby-lsp-test-exec
3838
bin/ruby-parse
3939
bin/ruby-rewrite
4040
bin/thor
41+
42+
gemfiles/*.lock

Gemfile.lock

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ PATH
22
remote: .
33
specs:
44
roast-ai (0.2.1)
5-
activesupport (~> 8.0)
5+
activesupport (~> 7.0)
66
cli-ui
77
diff-lcs (~> 1.5)
88
faraday-retry
@@ -13,7 +13,7 @@ PATH
1313
GEM
1414
remote: https://rubygems.org/
1515
specs:
16-
activesupport (8.0.2)
16+
activesupport (7.2.2.1)
1717
base64
1818
benchmark (>= 0.3)
1919
bigdecimal
@@ -25,7 +25,6 @@ GEM
2525
minitest (>= 5.1)
2626
securerandom (>= 0.3)
2727
tzinfo (~> 2.0, >= 2.0.5)
28-
uri (>= 0.13.1)
2928
addressable (2.8.7)
3029
public_suffix (>= 2.0.2, < 7.0)
3130
ast (2.4.3)

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ roast execute workflow.yml target_file.rb
124124
125125
# Or for a targetless workflow (API calls, data generation, etc.)
126126
roast execute workflow.yml
127+
128+
# Roast will automatically search in `project_root/roast/workflow_name` if the path is incomplete.
129+
roast execute my_cool_workflow # Equivalent to `roast execute roast/my_cool_workflow/workflow.yml
127130
```
128131

129132
### Understanding Workflows
@@ -476,9 +479,9 @@ Benefits of using OpenRouter:
476479

477480
When using OpenRouter, specify fully qualified model names including the provider prefix (e.g., `anthropic/claude-3-opus-20240229`).
478481

479-
#### Dynamic API Tokens
482+
#### Dynamic API Tokens and URIs
480483

481-
Roast allows you to dynamically fetch API tokens using shell commands directly in your workflow configuration:
484+
Roast allows you to dynamically fetch attributes such as API token and URI base (to use with a proxy) via shell commands directly in your workflow configuration:
482485

483486
```yaml
484487
# This will execute the shell command and use the result as the API token
@@ -490,8 +493,13 @@ api_token: $(echo $OPENAI_API_KEY)
490493
# For OpenRouter (requires api_provider setting)
491494
api_provider: openrouter
492495
api_token: $(echo $OPENROUTER_API_KEY)
493-
```
494496
497+
# Static Proxy URI
498+
uri_base: https://proxy.example.com/v1
499+
500+
# Dynamic Proxy URI
501+
uri_base: $(echo $AI_PROXY_URI_BASE)
502+
```
495503

496504
This makes it easy to use environment-specific tokens without hardcoding credentials, especially useful in development environments or CI/CD pipelines. Alternatively, Roast will fall back to `OPENROUTER_API_KEY` or `OPENAI_API_KEY` environment variables based on the specified provider.
497505

gemfiles/activesupport7.gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
eval_gemfile "../Gemfile"
4+
gem "activesupport", "~> 7.0"

gemfiles/activesupport8.gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
eval_gemfile "../Gemfile"
4+
gem "activesupport", "~> 8.0"

lib/roast.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ def execute(*paths)
2828
raise Thor::Error, "Workflow configuration file is required" if paths.empty?
2929

3030
workflow_path, *files = paths
31-
expanded_workflow_path = File.expand_path(workflow_path)
31+
32+
expanded_workflow_path = if workflow_path.include?("workflow.yml")
33+
File.expand_path(workflow_path)
34+
else
35+
File.expand_path("roast/#{workflow_path}/workflow.yml")
36+
end
37+
3238
raise Thor::Error, "Expected a Roast workflow configuration file, got directory: #{expanded_workflow_path}" if File.directory?(expanded_workflow_path)
3339

3440
Roast::Workflow::ConfigurationParser.new(expanded_workflow_path, files, options.transform_keys(&:to_sym)).begin!

lib/roast/tools/search_file.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ def call(glob_pattern, path = ".")
2929
Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{File.expand_path(path)}'\n")
3030
search_for(glob_pattern, path).then do |results|
3131
return "No results found for #{glob_pattern} in #{path}" if results.empty?
32-
return read_contents(results.first) if results.size == 1
32+
return read_contents(File.join(path, results.first)) if results.size == 1
3333

34-
results.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
34+
results.map { |result| File.join(path, result) }.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
3535
end
3636
rescue StandardError => e
3737
"Error searching for '#{glob_pattern}' in '#{path}': #{e.message}".tap do |error_message|

lib/roast/value_objects.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
require "roast/value_objects/api_token"
44
require "roast/value_objects/step_name"
55
require "roast/value_objects/workflow_path"
6+
require "roast/value_objects/uri_base"

lib/roast/value_objects/uri_base.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
module Roast
4+
module ValueObjects
5+
# Value object representing a URI base with validation
6+
class UriBase
7+
class InvalidUriBaseError < StandardError; end
8+
9+
attr_reader :value
10+
11+
def initialize(value)
12+
@value = value&.to_s
13+
validate!
14+
freeze
15+
end
16+
17+
def present?
18+
!blank?
19+
end
20+
21+
def blank?
22+
@value.nil? || @value.strip.empty?
23+
end
24+
25+
def to_s
26+
@value
27+
end
28+
29+
def ==(other)
30+
return false unless other.is_a?(UriBase)
31+
32+
value == other.value
33+
end
34+
alias_method :eql?, :==
35+
36+
def hash
37+
[self.class, @value].hash
38+
end
39+
40+
private
41+
42+
def validate!
43+
return if @value.nil? # Allow nil URI base, just not empty strings
44+
45+
raise InvalidUriBaseError, "URI base cannot be an empty string" if @value.strip.empty?
46+
end
47+
end
48+
end
49+
end

lib/roast/workflow/api_configuration.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
require "roast/factories/api_provider_factory"
44
require "roast/workflow/resource_resolver"
5+
require "roast/value_objects/uri_base"
56

67
module Roast
78
module Workflow
89
# Handles API-related configuration including tokens and providers
910
class ApiConfiguration
10-
attr_reader :api_token, :api_provider
11+
attr_reader :api_token, :api_provider, :uri_base
1112

1213
def initialize(config_hash)
1314
@config_hash = config_hash
@@ -37,6 +38,7 @@ def effective_token
3738
def process_api_configuration
3839
extract_api_token
3940
extract_api_provider
41+
extract_uri_base
4042
end
4143

4244
def extract_api_token
@@ -49,6 +51,12 @@ def extract_api_provider
4951
@api_provider = Roast::Factories::ApiProviderFactory.from_config(@config_hash)
5052
end
5153

54+
def extract_uri_base
55+
if @config_hash["uri_base"]
56+
@uri_base = ResourceResolver.process_shell_command(@config_hash["uri_base"])
57+
end
58+
end
59+
5260
def environment_token
5361
if openai?
5462
ENV["OPENAI_API_KEY"]

lib/roast/workflow/configuration.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Configuration
1414
attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :function_configs, :model, :resource
1515
attr_accessor :target
1616

17-
delegate :api_provider, :openrouter?, :openai?, to: :api_configuration
17+
delegate :api_provider, :openrouter?, :openai?, :uri_base, to: :api_configuration
1818

1919
# Delegate api_token to effective_token for backward compatibility
2020
def api_token

lib/roast/workflow/workflow_initializer.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,19 @@ def api_client_already_configured?
8181
end
8282
end
8383

84+
def client_options
85+
{
86+
access_token: @configuration.api_token,
87+
uri_base: @configuration.uri_base&.to_s,
88+
}.compact
89+
end
90+
8491
def configure_openrouter_client
8592
$stderr.puts "Configuring OpenRouter client with token from workflow"
8693
require "open_router"
8794

88-
client = OpenRouter::Client.new(access_token: @configuration.api_token)
95+
client = OpenRouter::Client.new(client_options)
96+
8997
Raix.configure do |config|
9098
config.openrouter_client = client
9199
end
@@ -96,7 +104,8 @@ def configure_openai_client
96104
$stderr.puts "Configuring OpenAI client with token from workflow"
97105
require "openai"
98106

99-
client = OpenAI::Client.new(access_token: @configuration.api_token)
107+
client = OpenAI::Client.new(client_options)
108+
100109
Raix.configure do |config|
101110
config.openai_client = client
102111
end

roast.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
3636
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
3737
spec.require_paths = ["lib"]
3838

39-
spec.add_dependency("activesupport", "~> 8.0")
39+
spec.add_dependency("activesupport", ">= 7.0")
4040
spec.add_dependency("cli-ui")
4141
spec.add_dependency("diff-lcs", "~> 1.5")
4242
spec.add_dependency("faraday-retry")

test/roast/cli_test.rb

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require "mocha/minitest"
5+
require "roast"
6+
7+
class RoastCLITest < ActiveSupport::TestCase
8+
def test_execute_with_workflow_yml_path
9+
workflow_path = "path/to/workflow.yml"
10+
expanded_path = File.expand_path(workflow_path)
11+
12+
# Mock the ConfigurationParser to prevent actual execution
13+
mock_parser = mock("ConfigurationParser")
14+
mock_parser.expects(:begin!).once
15+
Roast::Workflow::ConfigurationParser.expects(:new).with(expanded_path, [], {}).returns(mock_parser)
16+
17+
# Make sure File.directory? returns false to avoid the directory error
18+
File.expects(:directory?).with(expanded_path).returns(false)
19+
20+
# Execute the CLI command
21+
cli = Roast::CLI.new
22+
cli.execute(workflow_path)
23+
end
24+
25+
def test_execute_with_conventional_path
26+
workflow_name = "my_workflow"
27+
conventional_path = "roast/#{workflow_name}/workflow.yml"
28+
expanded_path = File.expand_path(conventional_path)
29+
30+
# Mock the ConfigurationParser to prevent actual execution
31+
mock_parser = mock("ConfigurationParser")
32+
mock_parser.expects(:begin!).once
33+
Roast::Workflow::ConfigurationParser.expects(:new).with(expanded_path, [], {}).returns(mock_parser)
34+
35+
# Make sure File.directory? returns false to avoid the directory error
36+
File.expects(:directory?).with(expanded_path).returns(false)
37+
38+
# Execute the CLI command
39+
cli = Roast::CLI.new
40+
cli.execute(workflow_name)
41+
end
42+
43+
def test_execute_with_directory_path_raises_error
44+
workflow_path = "path/to/directory"
45+
expanded_path = File.expand_path("roast/#{workflow_path}/workflow.yml")
46+
47+
# Make the directory check return true to trigger the error
48+
File.expects(:directory?).with(expanded_path).returns(true)
49+
50+
# Execute the CLI command and expect an error
51+
cli = Roast::CLI.new
52+
assert_raises(Thor::Error) do
53+
cli.execute(workflow_path)
54+
end
55+
end
56+
57+
def test_execute_with_files_passes_files_to_parser
58+
workflow_path = "path/to/workflow.yml"
59+
expanded_path = File.expand_path(workflow_path)
60+
files = ["file1.rb", "file2.rb"]
61+
62+
# Mock the ConfigurationParser to prevent actual execution
63+
mock_parser = mock("ConfigurationParser")
64+
mock_parser.expects(:begin!).once
65+
Roast::Workflow::ConfigurationParser.expects(:new).with(expanded_path, files, {}).returns(mock_parser)
66+
67+
# Make sure File.directory? returns false to avoid the directory error
68+
File.expects(:directory?).with(expanded_path).returns(false)
69+
70+
# Execute the CLI command
71+
cli = Roast::CLI.new
72+
cli.execute(workflow_path, *files)
73+
end
74+
75+
def test_execute_with_options_passes_options_to_parser
76+
workflow_path = "path/to/workflow.yml"
77+
expanded_path = File.expand_path(workflow_path)
78+
options = { "verbose" => true, "concise" => false }
79+
80+
# Mock the ConfigurationParser to prevent actual execution
81+
mock_parser = mock("ConfigurationParser")
82+
mock_parser.expects(:begin!).once
83+
Roast::Workflow::ConfigurationParser.expects(:new).with(expanded_path, [], options.transform_keys(&:to_sym)).returns(mock_parser)
84+
85+
# Make sure File.directory? returns false to avoid the directory error
86+
File.expects(:directory?).with(expanded_path).returns(false)
87+
88+
# Create CLI with options
89+
cli = Roast::CLI.new([], options)
90+
cli.execute(workflow_path)
91+
end
92+
end

test/roast/helpers/prompt_loader_test.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55

66
class RoastHelpersPromptLoaderTest < ActiveSupport::TestCase
77
def setup
8+
@original_openai_key = ENV.delete("OPENAI_API_KEY")
89
@workflow_file = fixture_file("workflow/workflow.yml")
910
@test_file = fixture_file("test.rb")
1011
@workflow = build_workflow(@workflow_file, @test_file)
1112
end
1213

14+
def teardown
15+
ENV["OPENAI_API_KEY"] = @original_openai_key
16+
end
17+
1318
def build_workflow(workflow_file, test_file)
1419
parser = Roast::Workflow::ConfigurationParser.new(workflow_file, [test_file])
1520
parser.instance_variable_set(

test/roast/tools/search_file_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,14 @@ def teardown
8181

8282
test ".call returns file content for single match" do
8383
Roast::Tools::SearchFile.stubs(:search_for).with("test_file1.txt", ".").returns(["test_file1.txt"])
84-
Roast::Tools::SearchFile.stubs(:read_contents).with("test_file1.txt").returns("file content")
84+
Roast::Tools::SearchFile.stubs(:read_contents).with("./test_file1.txt").returns("file content")
8585

8686
result = Roast::Tools::SearchFile.call("test_file1.txt")
8787
assert_equal "file content", result
8888
end
8989

9090
test ".call with path parameter passes path to search_for" do
91-
Roast::Tools::SearchFile.expects(:search_for).with("test_file", "nested/dir").returns(["nested/dir/test_file.txt"])
91+
Roast::Tools::SearchFile.expects(:search_for).with("test_file", "nested/dir").returns(["test_file.txt"])
9292
Roast::Tools::SearchFile.stubs(:read_contents).with("nested/dir/test_file.txt").returns("file content")
9393

9494
result = Roast::Tools::SearchFile.call("test_file", "nested/dir")
@@ -99,7 +99,7 @@ def teardown
9999
Roast::Tools::SearchFile.stubs(:search_for).with("file", ".").returns(["file1.txt", "file2.txt"])
100100

101101
result = Roast::Tools::SearchFile.call("file")
102-
assert_equal "file1.txt\nfile2.txt", result
102+
assert_equal "./file1.txt\n./file2.txt", result
103103
end
104104

105105
test ".call returns no results message when empty" do

0 commit comments

Comments
 (0)